<script setup lang="ts">
import faker from 'faker'
import Item from './components/item.vue'
import { ref, unref } from 'vue'
import { tryOnMounted } from '@vueuse/core'

const scroller = ref<HTMLElement | null>(null)

const BUFFER_SIZE = 3 // 「缓冲区元素」个数
const FIXED_HEIGHT = 180 // 固定高度

let listData: any = unref(ref([])) // 实际数组列表
let visibleData: any = ref([]) // 可视元素数组
let scrollRunwayEnd = ref(0) // 滚动结束的高度

let VISIBLE_COUNT = 0 // 可视元素个数

let firstAttachedItem = 0 // 「头挂载元素」
let lastAttachedItem = 0 // 「尾挂载元素」

let anchorItem = { index: 0, offset: 0 } // [锚点元素]初始值
let lastScrollTop = 0 // 上次滚动事件时 [滚动容器] 的 [滚动高度]

const fetchData = (count = 30) => {
  const result = []
  for (let i = 0; i < count; i++) {
    const item: any = faker.helpers.contextualCard()
    item.paragraph = faker.lorem.paragraph()
    result.push(item)
  }
  return result
}

// 给每个列表元素设置固定的下标
// const setItemIndex = (list: any) => {
//   let latestIndex = listData.length
//   for (let i = 0; i < list.length; i++) {
//     const item = list[i]
//     item.index = latestIndex + i
//     Object.freeze(item)
//   }
//   return list
// }

const calItemScrollY = (list: any) => {
  let latestIndex = listData.length
  for (let i = 0; i < list.length; i++) {
    const item = list[i]
    item.index = latestIndex + i
    item.scrollY = scrollRunwayEnd.value + i * FIXED_HEIGHT
    Object.freeze(item)
  }
  return list
}

const getFetchData = () => {
  listData.push(...calItemScrollY(fetchData()))
  scrollRunwayEnd.value = listData.length * FIXED_HEIGHT // 可滚动高度
}

// 列表到底加载更多数据
const handleLoadMore = (scrollTop: number) => {
  const scrollEnd = scrollTop + (scroller.value as HTMLElement).offsetHeight
  scrollEnd >= scrollRunwayEnd.value && getFetchData()
}

// [锚点元素]更新
const updateAnchorItem = (scrollTop: number) => {
  const index = Math.floor(scrollTop / FIXED_HEIGHT)
  const offset = scrollTop - index * FIXED_HEIGHT
  anchorItem = { index, offset }
}

const handleScroll = () => {
  const countScrollTop = (scroller.value as HTMLElement).scrollTop
  const differ = countScrollTop - lastScrollTop // 相差值
  lastScrollTop = countScrollTop

  // 更新[锚点元素]偏移量
  anchorItem.offset += differ

  const isPositive = differ >= 0 // 滚动条方向 +往下 -往上

  if (isPositive) {
    // 1.当[锚点元素]偏移量大于等于固定高度时，说明视图滚动条向下，并超过一个元素，需要更新[锚点元素]
    if (anchorItem.offset >= FIXED_HEIGHT) updateAnchorItem(countScrollTop)

    // 2.计算[头挂载元素]
    if (anchorItem.index - firstAttachedItem >= BUFFER_SIZE) {
      firstAttachedItem = Math.min(listData.length - VISIBLE_COUNT, anchorItem.index - BUFFER_SIZE)
    }
  } else {
    if (countScrollTop <= 0) {
      anchorItem = { index: 0, offset: 0 }
    }

    if (anchorItem.offset < 0) updateAnchorItem(countScrollTop)

    // 2.计算[头挂载元素]
    if (anchorItem.index - firstAttachedItem < BUFFER_SIZE) {
      firstAttachedItem = Math.max(0, anchorItem.index - BUFFER_SIZE)
    }
  }

  // 3.更新「尾挂载元素」
  lastAttachedItem = Math.min(firstAttachedItem + VISIBLE_COUNT + BUFFER_SIZE * 2, listData.length)
  // 4.更新「可视元素」
  visibleData.value = listData.slice(firstAttachedItem, lastAttachedItem)

  // 请求更多数据
  handleLoadMore(countScrollTop)
}

tryOnMounted(() => {
  VISIBLE_COUNT = Math.ceil((scroller.value as HTMLElement).offsetHeight / FIXED_HEIGHT)

  lastAttachedItem = VISIBLE_COUNT + BUFFER_SIZE

  getFetchData()

  visibleData.value = listData.slice(firstAttachedItem, lastAttachedItem)
})
</script>

<template>
  <ul class="height-fixed" ref="scroller" @scroll="handleScroll">
    <li
      class="height-fixed__scroll-runway"
      :style="`transform: translate(0, ${scrollRunwayEnd}px)`"
    ></li>
    <item
      class="height-fixed__item"
      v-for="item in visibleData"
      :data="item"
      :index="item.index"
      :key="item.username + item.phone"
      :style="`transform: translate(0, ${item.scrollY}px)`"
    />
  </ul>
</template>

<style lang="scss" scoped>
.height-fixed {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  margin: 0;
  padding: 0;
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
  position: absolute;
  background-color: #eee;
  contain: layout;
  will-change: transform;
  &__item,
  &__placeholder {
    position: absolute;
    contain: layout;
    will-change: transform;
  }
  &__scroll-runway {
    position: absolute;
    width: 1px;
    height: 1px;
    transition: transform 0.2s;
  }
}
</style>
